Вы — маркетинговый аналитик развлекательного приложения Procrastinate Pro+. Несмотря на огромные вложения в рекламу, последние несколько месяцев компания терпит убытки. Ваша задача — разобраться в причинах и помочь компании выйти в плюс.
1.Есть данные о пользователях, привлечённых с 1 мая по 27 октября 2019 года:
В качестве маркетингового аналитика развлекательного приложения Procrastinate Pro+ необходимо найти причины того, что несмотря на огромные вложения в рекламу, последние несколько месяцев компания терпит убытки. Задача — разобраться в причинах и помочь компании выйти в плюс.
На основе имеющихся данных определить, реклама с каких источяников является неэффективной и не окупается. Подготовить рекомендации для отдела маркетинга на основе анализа предоставленных данных.
Для работы в наше распоряжение переданы три датасета:
Структура visits_info_short.csv:
Структура orders_info_short.csv:
Структура costs_info_short.csv:
Загрузить данные о визитах, заказах и рекламных расходах из CSV-файлов в переменные.
Изучить данные и выполнить предобработку. Есть ли в данных пропуски и дубликаты? Убедиться, что типы данных во всех колонках соответствуют сохранённым в них значениям. Обратить внимание на столбцы с датой и временем.
Планируется использование функций, изученных в теоретических уроках.
Это функции для вычисления значений метрик:
get_profiles() — для создания профилей пользователей,get_retention() — для подсчёта Retention Rate,get_conversion() — для подсчёта конверсии,get_ltv() — для подсчёта LTV.А также функции для построения графиков:
filter_data() — для сглаживания данных,plot_retention() — для построения графика Retention Rate,plot_conversion() — для построения графика конверсии,plot_ltv_roi — для визуализации LTV и ROI.После каждого пункта сформулировать выводы.
Написать промежуточные выводы.
Используя графики LTV, ROI и CAC, проанализировать окупаемость рекламы. Условия: на календаре 1 ноября 2019 года, а в бизнес-плане заложено, что пользователи должны окупаться не позднее чем через две недели после привлечения. Необходимость включения в анализ органических пользователей определить самостоятельно.
Сделать вывод, описать возможные причины обнаруженных проблем и промежуточные рекомендации для рекламного отдела.
# импортируем все необходимые библиотеки
import pandas as pd
import matplotlib.pyplot as plt
from matplotlib import gridspec
%matplotlib inline
%config InlineBackend.figure_format='retina'
import seaborn as sns
import numpy as np
from scipy import stats as st
from datetime import datetime, timedelta
# прочитаем данные и переведём данные в нужный формат
visits = pd.read_csv('/datasets/visits_info_short.csv')
visits.columns = visits.columns.str.lower()
visits.rename(columns = {'user id':'user_id', 'session start':'session_start', 'session end':'session_end'}, inplace = True )
visits['session_start'] = pd.to_datetime(visits['session_start'])
visits['session_end'] = pd.to_datetime(visits['session_end'])
visits.info()
# убедимся в отсутствии дубликатов
visits.duplicated().sum()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 309901 entries, 0 to 309900 Data columns (total 6 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 user_id 309901 non-null int64 1 region 309901 non-null object 2 device 309901 non-null object 3 channel 309901 non-null object 4 session_start 309901 non-null datetime64[ns] 5 session_end 309901 non-null datetime64[ns] dtypes: datetime64[ns](2), int64(1), object(3) memory usage: 14.2+ MB
0
# прочитаем данные и переведём данные в нужный формат
orders = pd.read_csv('/datasets/orders_info_short.csv')
orders.columns = orders.columns.str.lower()
orders.rename(columns = {'user id':'user_id', 'event dt':'event_dt'}, inplace = True )
orders['event_dt'] = pd.to_datetime(orders['event_dt'])
orders.info()
# убедимся в отсутствии дубликатов
orders.duplicated().sum()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 40212 entries, 0 to 40211 Data columns (total 3 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 user_id 40212 non-null int64 1 event_dt 40212 non-null datetime64[ns] 2 revenue 40212 non-null float64 dtypes: datetime64[ns](1), float64(1), int64(1) memory usage: 942.6 KB
0
# прочитаем данные и переведём данные в нужный формат
costs = pd.read_csv('/datasets/costs_info_short.csv')
costs.columns = costs.columns.str.lower()
costs['dt'] = pd.to_datetime(costs['dt'])
costs.info()
# убедимся в отсутствии дубликатов
costs.duplicated().sum()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 1800 entries, 0 to 1799 Data columns (total 3 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 dt 1800 non-null datetime64[ns] 1 channel 1800 non-null object 2 costs 1800 non-null float64 dtypes: datetime64[ns](1), float64(1), object(1) memory usage: 42.3+ KB
0
Данные загружены в переменные и подготовлены к анализу:
Планируется использование функций, изученных в теоретических уроках.
Это функции для вычисления значений метрик:
get_profiles() — для создания профилей пользователей,get_retention() — для подсчёта Retention Rate,get_conversion() — для подсчёта конверсии,get_ltv() — для подсчёта LTV.# функция для создания пользовательских профилей
def get_profiles(visits, orders, costs):
# находим параметры первых посещений
profiles = (
visits.sort_values(by=['user_id', 'session_start'])
.groupby('user_id')
.agg(
{
'session_start': 'first',
'channel': 'first',
'device': 'first',
'region': 'first',
}
)
.rename(columns={'session_start': 'first_ts'})
.reset_index()
)
# для когортного анализа определяем дату первого посещения
# и первый день месяца, в который это посещение произошло
profiles['dt'] = profiles['first_ts'].dt.date
profiles['month'] = profiles['first_ts'].astype('datetime64[M]')
# добавляем признак платящих пользователей
profiles['payer'] = profiles['user_id'].isin(orders['user_id'].unique())
# считаем количество уникальных пользователей
# с одинаковым источником привлечения
un_users = (
profiles.groupby('channel')['user_id'].nunique().reset_index()
.merge(profiles[profiles['payer']==True].groupby('channel')['user_id'].nunique()
.reset_index(), on = 'channel', how = 'left')
)
un_users.set_axis(['channel', 'total_users', 'payers'], axis='columns', inplace=True)
un_users['payer_rate'] = un_users.payers / un_users.total_users
un_users.sort_values(by='payer_rate', ascending=False)
# объединяем траты на рекламу и число привлечённых пользователей
ad_costs = un_users.merge(costs.groupby('channel')['costs'].sum().reset_index(), on = 'channel', how = 'left')
# делим рекламные расходы на число привлечённых пользователей
ad_costs['acq_costs'] = ad_costs.costs / ad_costs.total_users
# добавляем стоимость привлечения в профили
profiles = profiles.merge(
ad_costs[['channel', 'acq_costs']],
on=['channel'],
how='left',
)
# стоимость привлечения органических пользователей равна нулю
profiles['acq_costs'] = profiles['acq_costs'].fillna(0)
return profiles
# составим профили пользователей
profiles = get_profiles(visits, orders, costs)
profiles.head(5)
| user_id | first_ts | channel | device | region | dt | month | payer | acq_costs | |
|---|---|---|---|---|---|---|---|---|---|
| 0 | 599326 | 2019-05-07 20:58:57 | FaceBoom | Mac | United States | 2019-05-07 | 2019-05-01 | True | 1.113286 |
| 1 | 4919697 | 2019-07-09 12:46:07 | FaceBoom | iPhone | United States | 2019-07-09 | 2019-07-01 | False | 1.113286 |
| 2 | 6085896 | 2019-10-01 09:58:33 | organic | iPhone | France | 2019-10-01 | 2019-10-01 | False | 0.000000 |
| 3 | 22593348 | 2019-08-22 21:35:48 | AdNonSense | PC | Germany | 2019-08-22 | 2019-08-01 | False | 1.008054 |
| 4 | 31989216 | 2019-10-02 00:07:44 | YRabbit | iPhone | United States | 2019-10-02 | 2019-10-01 | False | 0.218975 |
# функция для расчёта удержания
def get_retention(
profiles,
visits,
observation_date,
horizon_days,
dimensions=[],
ignore_horizon=False,
):
# добавляем столбец payer в передаваемый dimensions список
dimensions = ['payer'] + dimensions
# исключаем пользователей, не «доживших» до горизонта анализа
last_suitable_acquisition_date = observation_date
if not ignore_horizon:
last_suitable_acquisition_date = observation_date - timedelta(
days=horizon_days - 1
)
result_raw = profiles.query('dt <= @last_suitable_acquisition_date')
# собираем «сырые» данные для расчёта удержания
result_raw = result_raw.merge(
visits[['user_id', 'session_start']], on='user_id', how='left'
)
result_raw['lifetime'] = (
result_raw['session_start'] - result_raw['first_ts']
).dt.days
# функция для группировки таблицы по желаемым признакам
def group_by_dimensions(df, dims, horizon_days):
result = df.pivot_table(
index=dims, columns='lifetime', values='user_id', aggfunc='nunique'
)
cohort_sizes = (
df.groupby(dims)
.agg({'user_id': 'nunique'})
.rename(columns={'user_id': 'cohort_size'})
)
result = cohort_sizes.merge(result, on=dims, how='left').fillna(0)
result = result.div(result['cohort_size'], axis=0)
result = result[['cohort_size'] + list(range(horizon_days))]
result['cohort_size'] = cohort_sizes
return result
# получаем таблицу удержания
result_grouped = group_by_dimensions(result_raw, dimensions, horizon_days)
# получаем таблицу динамики удержания
result_in_time = group_by_dimensions(
result_raw, dimensions + ['dt'], horizon_days
)
# возвращаем обе таблицы и сырые данные
return result_raw, result_grouped, result_in_time
# функция для расчёта конверсии
def get_conversion(
profiles,
purchases,
observation_date,
horizon_days,
dimensions=[],
ignore_horizon=False,
):
# исключаем пользователей, не «доживших» до горизонта анализа
last_suitable_acquisition_date = observation_date
if not ignore_horizon:
last_suitable_acquisition_date = observation_date - timedelta(
days=horizon_days - 1
)
result_raw = profiles.query('dt <= @last_suitable_acquisition_date')
# определяем дату и время первой покупки для каждого пользователя
first_purchases = (
purchases.sort_values(by=['user_id', 'event_dt'])
.groupby('user_id')
.agg({'event_dt': 'first'})
.reset_index()
)
# добавляем данные о покупках в профили
result_raw = result_raw.merge(
first_purchases[['user_id', 'event_dt']], on='user_id', how='left'
)
# рассчитываем лайфтайм для каждой покупки
result_raw['lifetime'] = (
result_raw['event_dt'] - result_raw['first_ts']
).dt.days
# группируем по cohort, если в dimensions ничего нет
if len(dimensions) == 0:
result_raw['cohort'] = 'All users'
dimensions = dimensions + ['cohort']
# функция для группировки таблицы по желаемым признакам
def group_by_dimensions(df, dims, horizon_days):
result = df.pivot_table(
index=dims, columns='lifetime', values='user_id', aggfunc='nunique'
)
result = result.fillna(0).cumsum(axis = 1)
cohort_sizes = (
df.groupby(dims)
.agg({'user_id': 'nunique'})
.rename(columns={'user_id': 'cohort_size'})
)
result = cohort_sizes.merge(result, on=dims, how='left').fillna(0)
# делим каждую «ячейку» в строке на размер когорты
# и получаем conversion rate
result = result.div(result['cohort_size'], axis=0)
result = result[['cohort_size'] + list(range(horizon_days))]
result['cohort_size'] = cohort_sizes
return result
# получаем таблицу конверсии
result_grouped = group_by_dimensions(result_raw, dimensions, horizon_days)
# для таблицы динамики конверсии убираем 'cohort' из dimensions
if 'cohort' in dimensions:
dimensions = []
# получаем таблицу динамики конверсии
result_in_time = group_by_dimensions(
result_raw, dimensions + ['dt'], horizon_days
)
# возвращаем обе таблицы и сырые данные
return result_raw, result_grouped, result_in_time
# функция для расчёта LTV и ROI
def get_ltv(
profiles,
purchases,
observation_date,
horizon_days,
dimensions=[],
ignore_horizon=False,
):
# исключаем пользователей, не «доживших» до горизонта анализа
last_suitable_acquisition_date = observation_date
if not ignore_horizon:
last_suitable_acquisition_date = observation_date - timedelta(
days=horizon_days - 1
)
result_raw = profiles.query('dt <= @last_suitable_acquisition_date')
# добавляем данные о покупках в профили
result_raw = result_raw.merge(
purchases[['user_id', 'event_dt', 'revenue']], on='user_id', how='left'
)
# рассчитываем лайфтайм пользователя для каждой покупки
result_raw['lifetime'] = (
result_raw['event_dt'] - result_raw['first_ts']
).dt.days
# группируем по cohort, если в dimensions ничего нет
if len(dimensions) == 0:
result_raw['cohort'] = 'All users'
dimensions = dimensions + ['cohort']
# функция группировки по желаемым признакам
def group_by_dimensions(df, dims, horizon_days):
# строим «треугольную» таблицу выручки
result = df.pivot_table(
index=dims, columns='lifetime', values='revenue', aggfunc='sum'
)
# находим сумму выручки с накоплением
result = result.fillna(0).cumsum(axis=1)
# вычисляем размеры когорт
cohort_sizes = (
df.groupby(dims)
.agg({'user_id': 'nunique'})
.rename(columns={'user_id': 'cohort_size'})
)
# объединяем размеры когорт и таблицу выручки
result = cohort_sizes.merge(result, on=dims, how='left').fillna(0)
# считаем LTV: делим каждую «ячейку» в строке на размер когорты
result = result.div(result['cohort_size'], axis=0)
# исключаем все лайфтаймы, превышающие горизонт анализа
result = result[['cohort_size'] + list(range(horizon_days))]
# восстанавливаем размеры когорт
result['cohort_size'] = cohort_sizes
# собираем датафрейм с данными пользователей и значениями CAC,
# добавляя параметры из dimensions
cac = df[['user_id', 'acq_costs'] + dims].drop_duplicates()
# считаем средний CAC по параметрам из dimensions
cac = (
cac.groupby(dims)
.agg({'acq_costs': 'mean'})
.rename(columns={'acq_costs': 'cac'})
)
# считаем ROI: делим LTV на CAC
roi = result.div(cac['cac'], axis=0)
# удаляем строки с бесконечным ROI
roi = roi[~roi['cohort_size'].isin([np.inf])]
# восстанавливаем размеры когорт в таблице ROI
roi['cohort_size'] = cohort_sizes
# добавляем CAC в таблицу ROI
roi['cac'] = cac['cac']
# в финальной таблице оставляем размеры когорт, CAC
# и ROI в лайфтаймы, не превышающие горизонт анализа
roi = roi[['cohort_size', 'cac'] + list(range(horizon_days))]
# возвращаем таблицы LTV и ROI
return result, roi
# получаем таблицы LTV и ROI
result_grouped, roi_grouped = group_by_dimensions(
result_raw, dimensions, horizon_days
)
# для таблиц динамики убираем 'cohort' из dimensions
if 'cohort' in dimensions:
dimensions = []
# получаем таблицы динамики LTV и ROI
result_in_time, roi_in_time = group_by_dimensions(
result_raw, dimensions + ['dt'], horizon_days
)
return (
result_raw, # сырые данные
result_grouped, # таблица LTV
result_in_time, # таблица динамики LTV
roi_grouped, # таблица ROI
roi_in_time, # таблица динамики ROI
)
А также функции для построения графиков:
filter_data() — для сглаживания данных,plot_retention() — для построения графика Retention Rate,plot_conversion() — для построения графика конверсии,plot_ltv_roi — для визуализации LTV и ROI.# функция для сглаживания фрейма
def filter_data(df, window):
# для каждого столбца применяем скользящее среднее
for column in df.columns.values:
df[column] = df[column].rolling(window).mean()
return df
# функция для визуализации удержания
def plot_retention(retention, retention_history, horizon, window=7):
# задаём размер сетки для графиков
plt.figure(figsize=(15, 10))
# исключаем размеры когорт и удержание первого дня
retention = retention.drop(columns=['cohort_size', 0])
# в таблице динамики оставляем только нужный лайфтайм
retention_history = retention_history.drop(columns=['cohort_size'])[
[horizon - 1]
]
# если в индексах таблицы удержания только payer,
# добавляем второй признак — cohort
if retention.index.nlevels == 1:
retention['cohort'] = 'All users'
retention = retention.reset_index().set_index(['cohort', 'payer'])
# в таблице графиков — два столбца и две строки, четыре ячейки
# в первой строим кривые удержания платящих пользователей
ax1 = plt.subplot(2, 2, 1)
retention.query('payer == True').droplevel('payer').T.plot(
grid=True, ax=ax1
)
plt.legend()
plt.xlabel('Лайфтайм')
plt.title('Удержание платящих пользователей')
# во второй ячейке строим кривые удержания неплатящих
# вертикальная ось — от графика из первой ячейки
ax2 = plt.subplot(2, 2, 2, sharey=ax1)
retention.query('payer == False').droplevel('payer').T.plot(
grid=True, ax=ax2
)
plt.legend()
plt.xlabel('Лайфтайм')
plt.title('Удержание неплатящих пользователей')
# в третьей ячейке — динамика удержания платящих
ax3 = plt.subplot(2, 2, 3)
# получаем названия столбцов для сводной таблицы
columns = [
name
for name in retention_history.index.names
if name not in ['dt', 'payer']
]
# фильтруем данные и строим график
filtered_data = retention_history.query('payer == True').pivot_table(
index='dt', columns=columns, values=horizon - 1, aggfunc='mean'
)
filter_data(filtered_data, window).plot(grid=True, ax=ax3)
plt.xlabel('Дата привлечения')
plt.title(
'Динамика удержания платящих пользователей на {}-й день'.format(
horizon
)
)
# в чётвертой ячейке — динамика удержания неплатящих
ax4 = plt.subplot(2, 2, 4, sharey=ax3)
# фильтруем данные и строим график
filtered_data = retention_history.query('payer == False').pivot_table(
index='dt', columns=columns, values=horizon - 1, aggfunc='mean'
)
filter_data(filtered_data, window).plot(grid=True, ax=ax4)
plt.xlabel('Дата привлечения')
plt.title(
'Динамика удержания неплатящих пользователей на {}-й день'.format(
horizon
)
)
plt.tight_layout()
plt.show()
# функция для визуализации конверсии
def plot_conversion(conversion, conversion_history, horizon, window=7):
# задаём размер сетки для графиков
plt.figure(figsize=(15, 5))
# исключаем размеры когорт
conversion = conversion.drop(columns=['cohort_size'])
# в таблице динамики оставляем только нужный лайфтайм
conversion_history = conversion_history.drop(columns=['cohort_size'])[
[horizon - 1]
]
# первый график — кривые конверсии
ax1 = plt.subplot(1, 2, 1)
conversion.T.plot(grid=True, ax=ax1)
plt.legend()
plt.xlabel('Лайфтайм')
plt.title('Конверсия пользователей')
# второй график — динамика конверсии
ax2 = plt.subplot(1, 2, 2, sharey=ax1)
columns = [
# столбцами сводной таблицы станут все столбцы индекса, кроме даты
name for name in conversion_history.index.names if name not in ['dt']
]
filtered_data = conversion_history.pivot_table(
index='dt', columns=columns, values=horizon - 1, aggfunc='mean'
)
filter_data(filtered_data, window).plot(grid=True, ax=ax2)
plt.xlabel('Дата привлечения')
plt.title('Динамика конверсии пользователей на {}-й день'.format(horizon))
plt.tight_layout()
plt.show()
# функция для визуализации LTV и ROI
def plot_ltv_roi(ltv, ltv_history, roi, roi_history, horizon, window=7):
# задаём сетку отрисовки графиков
plt.figure(figsize=(20, 10))
# из таблицы ltv исключаем размеры когорт
ltv = ltv.drop(columns=['cohort_size'])
# в таблице динамики ltv оставляем только нужный лайфтайм
ltv_history = ltv_history.drop(columns=['cohort_size'])[[horizon - 1]]
# стоимость привлечения запишем в отдельный фрейм
cac_history = roi_history[['cac']]
# из таблицы roi исключаем размеры когорт и cac
roi = roi.drop(columns=['cohort_size', 'cac'])
# в таблице динамики roi оставляем только нужный лайфтайм
roi_history = roi_history.drop(columns=['cohort_size', 'cac'])[
[horizon - 1]
]
# первый график — кривые ltv
ax1 = plt.subplot(2, 3, 1)
ltv.T.plot(grid=True, ax=ax1)
plt.legend()
plt.xlabel('Лайфтайм')
plt.title('LTV')
# второй график — динамика ltv
ax2 = plt.subplot(2, 3, 2, sharey=ax1)
# столбцами сводной таблицы станут все столбцы индекса, кроме даты
columns = [name for name in ltv_history.index.names if name not in ['dt']]
filtered_data = ltv_history.pivot_table(
index='dt', columns=columns, values=horizon - 1, aggfunc='mean'
)
filter_data(filtered_data, window).plot(grid=True, ax=ax2)
plt.xlabel('Дата привлечения')
plt.title('Динамика LTV пользователей на {}-й день'.format(horizon))
# третий график — динамика cac
ax3 = plt.subplot(2, 3, 3, sharey=ax1)
# столбцами сводной таблицы станут все столбцы индекса, кроме даты
columns = [name for name in cac_history.index.names if name not in ['dt']]
filtered_data = cac_history.pivot_table(
index='dt', columns=columns, values='cac', aggfunc='mean'
)
filter_data(filtered_data, window).plot(grid=True, ax=ax3)
plt.xlabel('Дата привлечения')
plt.title('Динамика стоимости привлечения пользователей')
# четвёртый график — кривые roi
ax4 = plt.subplot(2, 3, 4)
roi.T.plot(grid=True, ax=ax4)
plt.axhline(y=1, color='red', linestyle='--', label='Уровень окупаемости')
plt.legend()
plt.xlabel('Лайфтайм')
plt.title('ROI')
# пятый график — динамика roi
ax5 = plt.subplot(2, 3, 5, sharey=ax4)
# столбцами сводной таблицы станут все столбцы индекса, кроме даты
columns = [name for name in roi_history.index.names if name not in ['dt']]
filtered_data = roi_history.pivot_table(
index='dt', columns=columns, values=horizon - 1, aggfunc='mean'
)
filter_data(filtered_data, window).plot(grid=True, ax=ax5)
plt.axhline(y=1, color='red', linestyle='--', label='Уровень окупаемости')
plt.xlabel('Дата привлечения')
plt.title('Динамика ROI пользователей на {}-й день'.format(horizon))
plt.tight_layout()
plt.show()
Исследуем данные по четырём параметрам: времени привлечения клиентов, географии (странам) привлечения, устройствам для выхода в приложение и источникам привлечения клиентов.
# посмотрим временные параметры данных по привлечению клиентов
profiles.groupby('channel').agg(
{'dt': ['min', 'max']}
)
| dt | ||
|---|---|---|
| min | max | |
| channel | ||
| AdNonSense | 2019-05-01 | 2019-10-27 |
| FaceBoom | 2019-05-01 | 2019-10-27 |
| LeapBob | 2019-05-01 | 2019-10-27 |
| MediaTornado | 2019-05-01 | 2019-10-27 |
| OppleCreativeMedia | 2019-05-01 | 2019-10-27 |
| RocketSuperAds | 2019-05-01 | 2019-10-27 |
| TipTop | 2019-05-01 | 2019-10-27 |
| WahooNetBanner | 2019-05-01 | 2019-10-27 |
| YRabbit | 2019-05-01 | 2019-10-27 |
| lambdaMediaAds | 2019-05-01 | 2019-10-27 |
| organic | 2019-05-01 | 2019-10-27 |
Минимальная и максимальная даты привлечения клиентов соответствуют прописаным в техническом задании. Со всех платформ,включая органическую,пользователи приходили с 01.05.2019 года по 27.10.2019 года согласно предоставленным данным.
# посмотрим географию привлечения пользователей
temp = profiles.groupby(['region']).agg({'user_id': 'count','payer':'sum'})
temp = temp.assign(percent = temp.payer / temp.user_id * 100).round(2)
temp = temp.sort_values(['percent', 'region'], ascending=[False, True])
temp
| user_id | payer | percent | |
|---|---|---|---|
| region | |||
| United States | 100002 | 6902 | 6.90 |
| Germany | 14981 | 616 | 4.11 |
| UK | 17575 | 700 | 3.98 |
| France | 17450 | 663 | 3.80 |
Пользователи приходят в приложение из Франции, Германии, Англии и США. Больше всего пользователей, в том числе платящих (6,90%), приходится на США. Меньше всего пользователей и самый низкий процент платящих (3,80%) - у Франции. Данные представлены в таблице выше.
# посмотрим устройства пользователей приложения
temp_1 = profiles.groupby(['device']).agg({'user_id': 'count','payer':'sum'})
temp_1 = temp_1.assign(percent = temp_1.payer / temp_1.user_id * 100).round(2)
temp_1 = temp_1.sort_values(['percent', 'device'], ascending=[False, True])
temp_1
| user_id | payer | percent | |
|---|---|---|---|
| device | |||
| Mac | 30042 | 1912 | 6.36 |
| iPhone | 54479 | 3382 | 6.21 |
| Android | 35032 | 2050 | 5.85 |
| PC | 30455 | 1537 | 5.05 |
Клиенты пользуются четырьмя типами устройств: iPhone, PC, Mac, Android. Платящие пользователи предпочитают iPhone. Процент платящих не самый большой, но пользователей с iPhone почти в 2 раза больше, чем пользователей любого другого устройства. Доли платящих пользователей составляют от 6,36% до 5,05%. Неохотнее других платят владельцы персональных компьютеров. Данные представлены в таблице выше.
# посмотрим на каналы привлечения клиентов
temp_2 = profiles.groupby(['channel']).agg({'user_id': 'count','payer':'sum'})
temp_2 = temp_2.assign(percent = temp_2.payer / temp_2.user_id * 100).round(2)
temp_2 = temp_2.sort_values(['percent', 'channel'], ascending=[False, True])
temp_2
| user_id | payer | percent | |
|---|---|---|---|
| channel | |||
| FaceBoom | 29144 | 3557 | 12.20 |
| AdNonSense | 3880 | 440 | 11.34 |
| lambdaMediaAds | 2149 | 225 | 10.47 |
| TipTop | 19561 | 1878 | 9.60 |
| RocketSuperAds | 4448 | 352 | 7.91 |
| WahooNetBanner | 8553 | 453 | 5.30 |
| YRabbit | 4312 | 165 | 3.83 |
| MediaTornado | 4364 | 156 | 3.57 |
| LeapBob | 8553 | 262 | 3.06 |
| OppleCreativeMedia | 8605 | 233 | 2.71 |
| organic | 56439 | 1160 | 2.06 |
costs.info()
costs
<class 'pandas.core.frame.DataFrame'> RangeIndex: 1800 entries, 0 to 1799 Data columns (total 3 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 dt 1800 non-null datetime64[ns] 1 channel 1800 non-null object 2 costs 1800 non-null float64 dtypes: datetime64[ns](1), float64(1), object(1) memory usage: 42.3+ KB
| dt | channel | costs | |
|---|---|---|---|
| 0 | 2019-05-01 | FaceBoom | 113.3 |
| 1 | 2019-05-02 | FaceBoom | 78.1 |
| 2 | 2019-05-03 | FaceBoom | 85.8 |
| 3 | 2019-05-04 | FaceBoom | 136.4 |
| 4 | 2019-05-05 | FaceBoom | 122.1 |
| ... | ... | ... | ... |
| 1795 | 2019-10-23 | lambdaMediaAds | 4.0 |
| 1796 | 2019-10-24 | lambdaMediaAds | 6.4 |
| 1797 | 2019-10-25 | lambdaMediaAds | 8.8 |
| 1798 | 2019-10-26 | lambdaMediaAds | 8.8 |
| 1799 | 2019-10-27 | lambdaMediaAds | 12.0 |
1800 rows × 3 columns
# считаем общую сумму расходов на маркетинг.
costs['costs'].sum()
105497.30000000002
Вывод: общая сумма расходов на маркетинг 105497,30 у.е.(условных единицы).
#считаем, как траты распределены по рекламным источникам
costs.groupby('channel').sum('costs')
| costs | |
|---|---|
| channel | |
| AdNonSense | 3911.25 |
| FaceBoom | 32445.60 |
| LeapBob | 1797.60 |
| MediaTornado | 954.48 |
| OppleCreativeMedia | 2151.25 |
| RocketSuperAds | 1833.00 |
| TipTop | 54751.30 |
| WahooNetBanner | 5151.00 |
| YRabbit | 944.22 |
| lambdaMediaAds | 1557.60 |
# построим график распределения рекламного бюджета по каналам методом барплот
costs1 = costs.pivot_table(
index='channel', values='costs', aggfunc='sum').sort_values(by='costs', ascending=False)
plt.figure(figsize=(15,5))
sns.barplot(x=costs1.index,y=costs1['costs'])
plt.title("Распределение рекламного бюджета по каналам")
plt.xlabel("Название канала")
plt.ylabel("Сумма вложений")
plt.xticks(rotation = 60)
plt.show()
Вывод: всего рекламных источников, по которым распределяются средства - 10. Самые большие вложения в исслдедуемый период осуществлялись в каналы TipTop (54751,3 у.е., больше половины всего рекламного бюджета) и FaceBoom (32445,6 у.е.).
# для посмтроения графиков добавим новые столбцы в датафрейм
costs['dt'] = pd.to_datetime(costs['dt'])
costs['month'] = costs['dt'].dt.month
costs['week'] = costs['dt'].dt.isocalendar().week
#Построим визуализацию динамики изменения расходов во времени (по неделям и месяцам) по каждому источнику
def plot_costs(costs):
costs = costs.drop(columns=['dt'])
fig = plt.figure(figsize=(12, 6))
gs = gridspec.GridSpec(ncols=2, nrows=1, figure=fig)
ax1 = fig.add_subplot(gs[0])
costs.pivot_table(
index='week',
columns='channel',
values='costs',
aggfunc='sum'
).plot(grid=True, ax=ax1)
ax1.set_title('Затраты на рекламу отдельно по каналам')
ax1.set_xlabel('неделя')
ax1.set_ylabel('Стоимость')
ax2 = fig.add_subplot(gs[1])
costs.pivot_table(
index='month',
columns='channel',
values='costs',
aggfunc='sum'
).plot(grid=True, ax=ax2)
ax2.set_title('Затраты на рекламу отдельно по каналам')
ax2.set_xlabel('месяц')
ax2.set_ylabel('Стоимость')
fig.tight_layout(pad=3.0)
plt.show()
plot_costs(costs)
Вывод: На графиках видно,как изменяются затраты на рекламу по каналам в динамике. На обоих графиках чётко просматриваются линии финансирования каналов TipTop и FaceBoom, устремлённые ввысь. Затраты по другим каналам не превышают сумм 300 у.е. в неделю и 1000 у.е. в месяц. Тогда как рекламные затраты по каналу FaceBoom достигают сумм 1800 у.е в неделю и свыше 6000 у.е. в месяц. Затраты на канал FaceBoom ещё выше - до 4000 у.е в неделю и свыше 13000 у.е. в месяц. Похоже,прибыль компании-заказчика получают эти два канала.
#Узнаем, сколько в среднем стоило привлечение одного пользователя (CAC) из каждого источника
profiles.pivot_table(
index='channel', values='acq_costs', aggfunc='median').sort_values(by='acq_costs', ascending=False)
| acq_costs | |
|---|---|
| channel | |
| TipTop | 2.799003 |
| FaceBoom | 1.113286 |
| AdNonSense | 1.008054 |
| lambdaMediaAds | 0.724802 |
| WahooNetBanner | 0.602245 |
| RocketSuperAds | 0.412095 |
| OppleCreativeMedia | 0.250000 |
| YRabbit | 0.218975 |
| MediaTornado | 0.218717 |
| LeapBob | 0.210172 |
| organic | 0.000000 |
Вывод: Таким образом, мы узнали, сколько в среднем стоило привлечение одного пользователя (CAC) из каждого источника. Стоимость пользователей из органических источников нулевая, поэтому учитывать её не будем. В колонке acq_costs приведены данные по средней стоимости привлечения в разбивке по рекламным каналам. Самые "дорогие" пользователи приходят в компанию с каналов TipTop (2.8 у.е.), FaceBoom (1,1 у.е.) и AdNonSense (1 у.е.). Меньше всего "стоят" пользователи, пришедшие с каналов MediaTornado, YRabbit и LeapBob (0.22-0.21 у.е.). Сделаем более подробный анализ и визуализируем его. Добавим расчёт стоимости привлечения "платящих" клиентов.
#сгруппируем пользователей по каналам
#и добавим новый столбец со средней стоимостью покупки пользователей
un_users = (
profiles.groupby('channel')['user_id'].nunique().reset_index()
.merge(profiles[profiles['payer']==True].groupby('channel')['user_id'].nunique()
.reset_index(), on = 'channel', how = 'left')
)
un_users.set_axis(['channel', 'total_users', 'payers'], axis='columns', inplace=True)
un_users['payer_rate'] = un_users.payers / un_users.total_users
un_users.sort_values(by='payer_rate', ascending=False)
| channel | total_users | payers | payer_rate | |
|---|---|---|---|---|
| 1 | FaceBoom | 29144 | 3557 | 0.122049 |
| 0 | AdNonSense | 3880 | 440 | 0.113402 |
| 9 | lambdaMediaAds | 2149 | 225 | 0.104700 |
| 6 | TipTop | 19561 | 1878 | 0.096007 |
| 5 | RocketSuperAds | 4448 | 352 | 0.079137 |
| 7 | WahooNetBanner | 8553 | 453 | 0.052964 |
| 8 | YRabbit | 4312 | 165 | 0.038265 |
| 3 | MediaTornado | 4364 | 156 | 0.035747 |
| 2 | LeapBob | 8553 | 262 | 0.030633 |
| 4 | OppleCreativeMedia | 8605 | 233 | 0.027077 |
| 10 | organic | 56439 | 1160 | 0.020553 |
# считаем, сколько в среднем стоило привлечение покупателей
# собираем таблицу
ad_costs = un_users.merge(costs.groupby('channel')['costs'].sum().reset_index(), on = 'channel', how = 'left')
ad_costs = ad_costs.query('channel != "organic"')
# считаем среднюю стоимость привлечения пользователя
#и платящего пользователя, добавляем новые столбцы
ad_costs['avg_costs'] = ad_costs.costs / ad_costs.payers
ad_costs['acq_costs'] = ad_costs.costs / ad_costs.total_users
# сортируем по среднему
ad_costs.sort_values(by='avg_costs', ascending=False)
| channel | total_users | payers | payer_rate | costs | avg_costs | acq_costs | |
|---|---|---|---|---|---|---|---|
| 6 | TipTop | 19561 | 1878 | 0.096007 | 54751.30 | 29.154047 | 2.799003 |
| 7 | WahooNetBanner | 8553 | 453 | 0.052964 | 5151.00 | 11.370861 | 0.602245 |
| 4 | OppleCreativeMedia | 8605 | 233 | 0.027077 | 2151.25 | 9.232833 | 0.250000 |
| 1 | FaceBoom | 29144 | 3557 | 0.122049 | 32445.60 | 9.121619 | 1.113286 |
| 0 | AdNonSense | 3880 | 440 | 0.113402 | 3911.25 | 8.889205 | 1.008054 |
| 9 | lambdaMediaAds | 2149 | 225 | 0.104700 | 1557.60 | 6.922667 | 0.724802 |
| 2 | LeapBob | 8553 | 262 | 0.030633 | 1797.60 | 6.861069 | 0.210172 |
| 3 | MediaTornado | 4364 | 156 | 0.035747 | 954.48 | 6.118462 | 0.218717 |
| 8 | YRabbit | 4312 | 165 | 0.038265 | 944.22 | 5.722545 | 0.218975 |
| 5 | RocketSuperAds | 4448 | 352 | 0.079137 | 1833.00 | 5.207386 | 0.412095 |
Определим стоимость привлечения каждого пользователя, платящего и не платящего.
# построим график стоимости привлечения клиентов каналами
ad_costs1 = ad_costs.pivot_table(
index='channel', values='acq_costs').sort_values(by='acq_costs', ascending=False)
plt.figure(figsize=(15,5))
sns.barplot(x=ad_costs1.index,y=ad_costs1['acq_costs'])
plt.title("Стоимость привлечения одного клиента рекламными каналами")
plt.xlabel("Название канала")
plt.ylabel("стоимость привлечения клиента")
plt.xticks(rotation = 60)
plt.show()
Используя графики LTV, ROI и CAC, проанализировать окупаемость рекламы. Условия: на календаре 1 ноября 2019 года, а в бизнес-плане заложено, что пользователи должны окупаться не позднее чем через две недели после привлечения. Необходимость включения в анализ органических пользователей определить самостоятельно.
Сделать вывод, описать возможные причины обнаруженных проблем и промежуточные рекомендации для рекламного отдела.
# установим момент и горизонт анализа
observation_date = datetime(2019, 11, 1).date() # момент анализа
horizon_days = 14 # горизонт анализа
# исключим из анализа органических пользователей
profiles_new = profiles.query('channel != "organic"')
Органических пользователей из анализа исключаем для "чистоты" подсчёта окупаемости затрат на рекламу.
Проанализируем окупаемость рекламы c помощью графиков LTV и ROI, а также графики динамики LTV, CAC и ROI.
# считаем LTV и ROI
ltv_raw, ltv_grouped, ltv_history, roi_grouped, roi_history = get_ltv(
profiles_new, orders, observation_date, horizon_days
)
# строим графики
plot_ltv_roi(ltv_grouped, ltv_history, roi_grouped, roi_history, horizon_days)
По графикам можно сделать такие выводы: Реклама не окупается. ROI в конце недели — даже не приблизилась к уровню окупаемости, примерно 80%. CAC незначительно увеличился в июне по сравнению с маем месяцем и стабилизироваля выше 1. Видимо, в июне был значительно увеличен рекламный бюджет. Показатель LTV стабилен. Значит, дело не в ухудшении качества пользователей. На последнем графике чётко видно, что ROI пользователей ниже уровня окупаемости весь период. Причины этого предстоит выяснить. Чтобы разобраться в причинах, пройдём по всем доступным характеристикам пользователей — устройству первого посещения, стране и источнику. Затем изучим конверсию и удержание пользователей в динамике её изменений.
Посчитаем окупаемостьс разбивкой по устройствам.
# смотрим окупаемость с разбивкой по устройствам
dimensions = ['device']
ltv_raw, ltv_grouped, ltv_history, roi_grouped, roi_history = get_ltv(
profiles_new, orders, observation_date, horizon_days, dimensions=dimensions
)
plot_ltv_roi(
ltv_grouped, ltv_history, roi_grouped, roi_history, horizon_days, window=14
)
Вывод: Стоимость привлечения владельцев iPhone и Mac несколько выше стоимости привлечения владельцев Android.При этом ROI и его динамика на 14-й деньсущественно ниже линии доходности в течение всего изучаемого периода. Владельцы PC демонстрируют самую низкую стоимость привлечения, при этом у пользователей этих устройств самый высокий ROI и его динамика. Только владельцы PC пересекают линию доходности, но только на 11 день Посмотрим с пристрастиемна конверсию и удержание владельцев iPhone и Mac.
# смотрим конверсию с разбивкой по устройствам
conversion_raw, conversion_grouped, conversion_history = get_conversion(
profiles_new, orders, observation_date, horizon_days, dimensions=dimensions
)
plot_conversion(conversion_grouped, conversion_history, horizon_days)
# смотрим удержание с разбивкой по устройствам
retention_raw, retention_grouped, retention_history = get_retention(
profiles_new, visits, observation_date, horizon_days, dimensions=dimensions
)
plot_retention(retention_grouped, retention_history, horizon_days)
Вывод: Конверсия владельцев iPhone и Mac выше показателей владельцев других устройств, но коэффициент удержания этих пользователей незначительно ниже.Владельцы PC демонстрируют самую низкую конверсию и саое высокое удержание. Стоит рекомендовать техподдержке приложения промониторить его работу на iPhone и Mac,возможно, там есть неудобства для пользователя.
Посчитаем окупаемостьс разбивкой по странам.
# смотрим окупаемость с разбивкой по странам
dimensions = ['region']
ltv_raw, ltv_grouped, ltv_history, roi_grouped, roi_history = get_ltv(
profiles_new, orders, observation_date, horizon_days, dimensions=dimensions
)
plot_ltv_roi(
ltv_grouped, ltv_history, roi_grouped, roi_history, horizon_days, window=14
)
Вывод. Сразу обращают на себя внимание пользователи из США: при более высоком, чем Англии, Германии и Франции LTV, стоимостьпривлечения пользователя из США существенно выше, а ROI - так и не достигает линии окупаемости, ни абсолютно, ни в динамике. Есть смысл перераспределить рекламный бюджет из Америки в Европу. Посмотрим с пристрастием на конверсию и удержание американских пользователей приложения.
# смотрим конверсию с разбивкой по странам
conversion_raw, conversion_grouped, conversion_history = get_conversion(
profiles_new, orders, observation_date, horizon_days, dimensions=dimensions
)
plot_conversion(conversion_grouped, conversion_history, horizon_days)
# смотрим удержание с разбивкой по странам
retention_raw, retention_grouped, retention_history = get_retention(
profiles_new, visits, observation_date, horizon_days, dimensions=dimensions
)
plot_retention(retention_grouped, retention_history, horizon_days)
Вывод: Пользователи США демонстрируют более высокую конверсию, чем европейцы. А вот удержание платящих пользователей существенно ниже, чем в Европе - и абсолютно, и в динамике.
Посчитаем окупаемостьс разбивкой по источникам привлечения.
# смотрим окупаемость с разбивкой по источникам привлечения
dimensions = ['channel']
ltv_raw, ltv_grouped, ltv_history, roi_grouped, roi_history = get_ltv(
profiles_new, orders, observation_date, horizon_days, dimensions=dimensions
)
plot_ltv_roi(
ltv_grouped, ltv_history, roi_grouped, roi_history, horizon_days, window=14
)
Вывод. Не смотря на высокий LTV пользователей ROI канала TipTop даже не приближается к линии окупаемости. Вместе с этим каналом ниже черты доходности остаются каналы FaceBoom и AdNonSense. Вложения в каналы OppleCreativeMedia, LeapBob и MediaTornado окупаются. Посмотрим конверсию и удержание пользователей, пришедших с платформ TipTop, FaceBoom и AdNonSense.
# смотрим конверсию с разбивкой по источникам привлечения
conversion_raw, conversion_grouped, conversion_history = get_conversion(
profiles_new, orders, observation_date, horizon_days, dimensions=dimensions
)
plot_conversion(conversion_grouped, conversion_history, horizon_days)
# смотрим удержание с разбивкой по источникам привлечения
retention_raw, retention_grouped, retention_history = get_retention(
profiles_new, visits, observation_date, horizon_days, dimensions=dimensions
)
plot_retention(retention_grouped, retention_history, horizon_days)
Вывод: Пользователи TipTop, FaceBoom и AdNonSense демонстрируют высокую конверсию. а вот с удержанием - всё по-разному. Пользователи платформ FaceBoom и AdNonSense демонстрируют крайне низкое удержание, вообще и в динамике на 14-й день.
В ходе анализа предоставленных данных сделаны следующие выводы:
Компания продаёт развлекательное приложение Procrastinate Pro+ на рынках 4-х крупнейших стран мира: Англии, Германии, Франции и США. При этом 3 из них находятся в Европе, а четвёртая - в Америке.
Из пользователей компании 50 006 пользователей находятся в Европе и 100 002 пользователя находятся в США. Пользователи Европы совокупно производят около 3000 покупок, а пользователи США - 6902 покупки. То есть, в США вдвое больше пользователей, чем в Европе. И покупок пользователи США совершают в 2 с лишним раза больше.
Однако, именно маркетинговая политика в США приводит к убыткам компании:
Таким образом,мы вычислили проблемную зону компании. Именно в филиале (или в отделе), работающем с пользователями США необходимо произвести изменения для оздоровления финансовой ситуации в компании.
В европейских странах нуждаются в ревизии каналы рекламы, стоит перераспределить рекламный бюджет.
Рекомендации для рекламного отдела.